//  IDVI 1.0 source copyright 1996 Garth A. Dickie
//
//  This source is free for non-commercial use.  No warranty, etc.
//  Please acknowledge reuse by including the line:
//
//  "Based in part on IDVI 1.0 source copyright 1996 Garth A. Dickie"
//
//  in your documentation and source code.  For commercial use or
//  distribution, please contact the author.  Please also send
//  questions, comments, bug reports, or fixes.
//
//  Best Regards,
//  Garth A. Dickie
//  dickie@elastic.avid.com

package ibook.v10.idvi.display;

import java.awt.Color;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.Panel;
import java.net.URL;
import java.net.MalformedURLException;
import java.applet.AppletContext;

import ibook.v10.idvi.AppletContainer;
import ibook.v10.idvi.IDVI;
import ibook.v10.idvi.IDVIContext;
import ibook.v10.idvi.ShowDocumentContext;
import ibook.v10.parameter.ParameterProcessor;

//  Margins:
//      Margins are set by using the xOffset and yOffset parameters to the constructor.
//      These parameters are in pixels, and are interpreted as a translation to be
//      applied to the text of the View.  This means that a negative value creates
//      a smaller margin than the default, and a positive value creates a larger margin
//      than the default.
//
//      The implementation only uses xOffset and yOffset when it interacts with the
//      outside environment.  This is in sendMouseEvent( ), where mouse coordinates
//      are translated, in paint( ), where a Graphics object is translated, in the
//      DVIRectangle version of repaint( ), where the rectangle is translated, and
//      in childChangedExpansion( ), where both a Graphics object and a repaint( )
//      rectangle are translated.

public class ViewPanel extends Panel implements IDVIContext, ShowDocumentContext {
    private BlockRoot           root;
    private int                 xOffset;
    private int                 yOffset;
    private IDVIContext         idviContext;
    private ShowDocumentContext showDocumentContext;
    private boolean             includeComponents;

    private BlockLock           lock;
    private ScaledColorScheme   scaledColorScheme;
    private View                view;
    private DVIRectangle        bounds;
    private boolean             getsMouseEvents;

    public ViewPanel(
            Block               block,
            BlockRoot           root,
            ColorScheme         colorScheme,
            int                 scale,
            int                 xOffset,
            int                 yOffset,
            IDVIContext         idviContext,
            ShowDocumentContext showDocumentContext,
            boolean             includeComponents ) {

        this.root                = root;
        this.xOffset             = xOffset;
        this.yOffset             = yOffset;
        this.idviContext         = idviContext;
        this.showDocumentContext = showDocumentContext;
        this.includeComponents   = includeComponents;

        lock = root.getBlockLock( );
        scaledColorScheme = colorScheme.getScaledColorScheme( scale );

        setLayout( null );
        setForeground( scaledColorScheme.getForegroundColor( ScaledColorScheme.kIndexText ));
        setBackground( scaledColorScheme.getBackgroundColor( ScaledColorScheme.kIndexText ));

        lock.getGetViewLock( );

        view = block.getView( scaledColorScheme, scale, this );

        ViewPanelView relay = new ViewPanelView( this );
        view.setParent( relay, 0 );
        int flags = view.getFlags( );
        getsMouseEvents = ( flags & View.kFlagGetsMouseEvents ) != 0;

        bounds = new DVIRectangle( );
        int expansion = view.getBounds( bounds, scale, 100, 100, block );
        bounds.offsetBottom( expansion );

        view.showComponents( xOffset, yOffset );

        lock.putGetViewLock( );
    }




    //  setShowDocumentContext( ) exists because browsers (Netscape) won't respond
    //  to showDocument requests from applets which are not on the current page.
    //  So when a ViewPanel is in the zoom window, and the user has changed pages
    //  and clicks on a link in the zoom window, it won't work.  The fix is to have
    //  the zoom window call setShowDocumentContext( ) with a fresher context,
    //  whenever the page changes.

    public void setShowDocumentContext( ShowDocumentContext showDocumentContext ) {
        this.showDocumentContext = showDocumentContext;
    }




    //  IDVIContext implementation.

    public AppletContext getAppletContext( ) {
        return idviContext.getAppletContext( );
    }

    public ParameterProcessor getParameterProcessor( ) {
        return idviContext.getParameterProcessor( );
    }

    public URL getCodeBase( ) {
        return idviContext.getCodeBase( );
    }

    public URL getDVIDocumentBase( ) {
        return idviContext.getDVIDocumentBase( );
    }

    public void registerAppletContainer( AppletContainer container ) {
        idviContext.registerAppletContainer( container );
    }




    //  ShowDocumentContext implementation

    public void showDocument( String urlString, String target ) {
        boolean used = false;

        if( urlString.regionMatches( true, 0, IDVI.kIDVIURLPrefix, 0, IDVI.kIDVIURLPrefix.length( )))
            used = processSpecialURL( urlString.substring( IDVI.kIDVIURLPrefix.length( )));

        if( ! used && showDocumentContext != null )
            showDocumentContext.showDocument( urlString, target );
    }




    //  getLock( ) is provided for children which need to obtain a BlockLock lock
    //  asynchronously.

    BlockLock getLock( ) {
        return lock;
    }

    boolean getIncludeComponents( ) {
        return includeComponents;
    }

    //  setOffsets( ) is provided for use by the ZoomPanel class, derived from ViewPanel.

    void setOffsets( int newXOffset, int newYOffset ) {
        int xDelta = newXOffset - xOffset;
        int yDelta = newYOffset - yOffset;

        boolean showing = isShowing( );

        if( xDelta != 0 || yDelta != 0 ) {
            lock.getModifyStateLock( );

            xOffset = newXOffset;
            yOffset = newYOffset;

            if( showing ) {
                Graphics g = getGraphics( );

                int width = size( ).width;
                int height = size( ).height;

                if( Math.abs( xDelta ) < width && Math.abs( yDelta ) < height ) {
                    g.copyArea( 0, 0, width, height, xDelta, yDelta );

                    if( xDelta > 0 ) {
                        g.clearRect( 0, 0, xDelta, height );
                        repaint( 0, 0, xDelta, height );
                    } else if( xDelta < 0 ) {
                        g.clearRect( width + xDelta, 0, - xDelta, height );
                        repaint( width + xDelta, 0, - xDelta, height );
                    }
                    
                    if( yDelta > 0 ) {
                        int x = Math.max( 0, xDelta );
                        int w = width - Math.abs( xDelta );

                        g.clearRect( x, 0, w, yDelta );
                        repaint( x, 0, w, yDelta );
                    } else if( yDelta < 0 ) {
                        int x = Math.max( 0, xDelta );
                        int w = width - Math.abs( xDelta );

                        g.clearRect( x, height + yDelta, w, - yDelta );
                        repaint( x, height + yDelta, w, - yDelta );
                    }
                } else {
                    g.clearRect( 0, 0, width, height );
                    repaint( );
                }
            }
            view.showComponents( xOffset, yOffset );
            lock.putModifyStateLock( );
        }
    }




    //  paint( ), update( ), and handleEvent( )  are overrides of Component methods,
    //  and are called by the runtime environment.

    private DVIRectangle        paintBounds = new DVIRectangle( );

    public void paint( Graphics g ) {
        DVIRectangle clip = new DVIRectangle( g.getClipRect( ));

        //  The macintosh java implementation doesn't offset the cliprect
        //  properly when a graphics object is translated, so we grab the
        //  cliprect beforehand and offset it manually.
        g.translate( xOffset, yOffset );
        clip.offsetX( - xOffset );
        clip.offsetY( - yOffset );

        lock.getPaintLock( );
        paintBounds.set( bounds );
        view.paint( g, ScaledColorScheme.kIndexText, paintBounds, clip, 0 );
        lock.putPaintLock( );
    }

    //  Normally we don't want to erase the display before redrawing it.  But
    //  sometimes the viewChangedExpansion method can't update the display as
    //  it should because it can't obtain a Graphics object.  In that case,
    //  it issues a repaint request, with the eraseDisplay flag set to true.

    private boolean eraseDisplay = false;

    public void update( Graphics g ) {
        if( eraseDisplay ) {
            eraseDisplay = false;
            g.clearRect( 0, 0, size( ).width, size( ).height );
        }

        paint( g );
    }

    public boolean handleEvent( Event e ) {
        boolean result = false;

        switch( e.id ) {
            case Event.MOUSE_ENTER:
            case Event.MOUSE_MOVE:
            case Event.MOUSE_DOWN:
            case Event.MOUSE_DRAG:
            case Event.MOUSE_UP:
            case Event.MOUSE_EXIT:
                //  We only ask the View heierarchy to handle a mouse event
                //  if the event was aimed at this Panel.  We will also
                //  receive events for any sub-components, such as applets,
                //  if the sub-components do not consume the events.

                if( e.target == this && getsMouseEvents )
                    result = sendMouseEvent( e );
                break;
            default:
                break;
        }

        return result;
    }




    //  viewAddedToBounds( ), viewAddedToFlags( ), viewChangedExpansion( ),
    //  childBounds( ), and repaint( ) are called by the ViewPanelView associated
    //  with this ViewPanel, when the similarly-named methods of the ViewPanelView
    //  are called by a subview.

    void viewAddedToBounds( DVIRectangle newBounds, int expansion ) {
        bounds.set( newBounds );
        bounds.offsetBottom( expansion );
    }

    void viewAddedToFlags( int newFlags ) {
        getsMouseEvents |= ( newFlags & View.kFlagGetsMouseEvents ) != 0;

        if(( newFlags & View.kFlagHasComponents ) != 0 )
            view.showComponents( xOffset, yOffset );
    }

    void viewChangedExpansion( int expansionDelta, int yPosition ) {
        if( ! bounds.unset && ( expansionDelta > 0 || yPosition < bounds.bottom || yPosition - expansionDelta < bounds.bottom )) {
            int width = bounds.right - bounds.left;

            Graphics g = getGraphics( );
            if( g != null ) {
                g.translate( xOffset, yOffset );

                if( expansionDelta > 0 ) {
                    g.copyArea( bounds.left, yPosition, width, bounds.bottom - yPosition, 0, expansionDelta );
                    g.clearRect( bounds.left, yPosition, width, expansionDelta );
                    repaint( bounds.left + xOffset, yPosition + yOffset, width, expansionDelta );
                } else {
                    int top = yPosition - expansionDelta;

                    if( top < bounds.bottom )
                        g.copyArea( bounds.left, top, width, bounds.bottom - top, 0, expansionDelta );

                    if( yPosition < bounds.bottom )
                        g.clearRect( bounds.left, bounds.bottom + expansionDelta, width, - expansionDelta );
                }
                g.dispose( );

                //  Replace the two repaint( ) call above with this one, if things get wierd.
                //  This one repaints the whole area which was scrolled, while the one above
                //  requests only a repaint for the area we know is newly exposed.
                //
                //  repaint( bounds.left + xOffset, yPosition + yOffset, width, bounds.bottom + expansionDelta - yPosition );
            } else {
                System.out.println( "Could not obtain Graphics object.  Display may be out of date." );
                eraseDisplay = true;
                repaint( );
            }
        }

        if( ! bounds.unset )
            bounds.offsetBottom( expansionDelta );
    }

    void childBounds( DVIRectangle childBounds ) {
        childBounds.set( bounds );
    }

    void repaint( DVIRectangle repaintBounds, long delay ) {
        int x = repaintBounds.left + xOffset;
        int y = repaintBounds.top  + yOffset;

        int width  = repaintBounds.right  - repaintBounds.left;
        int height = repaintBounds.bottom - repaintBounds.top;

        repaint( delay, x, y, width, height );
    }

    //  printStructure is used by the TestApplet classes to
    //  display the entire structure of a View heierarchy.

    public void printStructure( ) {
        System.out.println( getClass( ).getName( ) + ", bounds = " + bounds );
        view.printStructure( "  " );    
    }




    //  private mouse handling routines:
    //
    //  We try to maintain some invariants for the mouse handling
    //  routines in the View objects, to make their life easier.
    //  For example, mouseDrag always occurs between mouseDown and mouseUp,
    //  mouseDown and mouseUp are always in pairs, mouseEnter and mouseExit
    //  are always in pairs, and there is always a mouseEnter before a
    //  mouseMove or a mouseDown.

    private boolean lastMouseInside = false;
    private boolean lastMouseDown = false;

    private boolean sendMouseEvent( Event e ) {
        boolean result = false;

        int x = e.x - xOffset;
        int y = e.y - yOffset;

        lock.getModifyStateLock( );
        switch( e.id ) {
            case Event.MOUSE_ENTER:
            case Event.MOUSE_MOVE:
            case Event.MOUSE_DRAG:
                sendMousePosition( x, y );
                break;
            case Event.MOUSE_DOWN:
                result = sendMouseDown( x, y, e.clickCount );
                break;
            case Event.MOUSE_UP:
                result = sendMouseUp( x, y, e.modifiers );
                break;
            case Event.MOUSE_EXIT:
                sendMouseExit( );
                break;
        }
        lock.putModifyStateLock( );

        return result;
    }

    private void sendMousePosition( int x, int y ) {
        if( lastMouseDown )
            view.mouseDrag( x, y );
        else if( lastMouseInside )
            view.mouseMove( x, y );
        else {
            view.mouseEnter( x, y );
            lastMouseInside = true;
        }
    }

    private boolean sendMouseDown( int x, int y, int clickCount ) {
        sendMousePosition( x, y );
        boolean result = lastMouseDown || view.mouseDown( clickCount );
        lastMouseDown = true;

        return result;
    }

    //  On a mouseUp, the mouse may be located far away from the
    //  original subView which got the mouseDown.  We do a second
    //  sendMousePosition( ) after changing the lastMouseDown flag
    //  so that an appropriate mouseEnter( ) might get sent to a new
    //  subView.

    private boolean sendMouseUp( int x, int y, int modifiers ) {
        sendMousePosition( x, y );
        boolean result = ! lastMouseDown || view.mouseUp( modifiers );
        lastMouseDown = false;
        sendMousePosition( x, y );

        return result;
    }

    private void sendMouseExit( ) {
        if( lastMouseInside ) view.mouseExit( );
        lastMouseInside = false;
    }




    //  Handling special URLs.  Most special URLs have to do with page and scale
    //  navigation, and are dealt with by the applet.  The ViewPanel catches and
    //  handles idvi:toggle: URLs, which are generated when the reader clicks on
    //  the term part of a term/definition pair in an outline.

    private boolean processSpecialURL( String urlString ) {
        boolean used = false;

        if( urlString.regionMatches( true, 0, IDVI.kToggleURLPrefix, 0, IDVI.kToggleURLPrefix.length( ))) {
            ToggleBlock block = ( ToggleBlock ) root.getNamedBlock( IDVI.kIDVIURLPrefix + urlString );

            if( block != null )
                block.toggle( );

            used = true;
        }

        return used;
    }
}
